package zen_group.mdl;

import static java.lang.Math.*;

import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Vector;
import java.util.HashMap;

public
class Part
        implements Comparable<Part>
{
    public enum Fix { FIFO, FILO };

    static public
    class Fault
            implements Cloneable
    {
        private int mDay;
        private int mCount;


        public
        Fault(int day, int count)
        {
            mDay    = day;
            mCount  = count;
        }


        /**
         *  0-based index of day from period start.
         */
        public int
        day()
        {
            return mDay;
        }


        /**
         *  Fault count at specified day().
         */
        public int
        count()
        {
            return mCount;
        }


        public String
        toString()
        {
            return day() + ":" + count();
        }
    }


    static public
    class Income
    {
        //public enum Tactic { FAIR, PROFIT };

        private float   mAll    = 0;
        private float   mPure   = 0;


        public
        Income(float all, float pure)
        {
            mAll    = all;
            mPure   = pure;
        }


        /**
         *  All income.
         */
        public float
        all()
        {
            return mAll;
        }


        /**
         *  Pure profit.
         */
        public float
        pure()
        {
            return mPure;
        }
    }


    public
    class Period
            implements Comparable<Period>
    {
        private
        Period(Vector<Fault> faults)
        {
            mFaults = (null != faults) ? faults : new Vector<Fault>();

            for (Fault f : mFaults)
            {
                mDeficits.put(
                        f.count(),
                        mDeficits.containsKey(f.count())
                            ? new Integer(mDeficits.get(f.count()) + 1)
                            : new Integer(1)
                    );
            }

            mIncome = new Vector<Income>(5);

            Income  lastIncome  = getIncome(0);
            mIncome.add(lastIncome);
            Income  income;
            for (int spare = 1;
                    lastIncome.pure() < (income = getIncome(spare)).pure();
                    mIncome.add(income), lastIncome = income, ++spare)
                ;   // NO BODY
        }


        /**
         *  Vector of Faults occured in period.
         */
        public Vector<Fault>
        faults()
        {
            return mFaults;
        }


        /**
         *  Vector of Income. Vector index indicates number of parts
         *  reserved in warehouse.
         *
         *  Last element index indicates the optimal amount of reserved
         *  parts.
         */
        public Vector<Income>
        income()
        {
            return mIncome;
        }


        /**
         *  Map of deficits to number of times.
         */
        public HashMap<Integer, Integer>
        deficits()
        {
            return mDeficits;
        }


        public int
        compareTo(Period other)
        {
            return income().size() - other.income().size();
        }


        private Income
        getIncome(int spare)
        {
            Warehouse   wh      = new Warehouse(spare, price(), mLoss);
            float       income  = wh.process(mFaults);
            return new Income(income, income - wh.expenses());
        }

        private Vector<Fault>   mFaults;
        private Vector<Income>  mIncome;
        private HashMap<Integer, Integer>   mDeficits   =
                new HashMap<Integer, Integer>();
    }


    static private
    class Warehouse
    {
        public
        Warehouse(int spare, float price, float loss)
        {
            mSpare          = spare;
            mPrice          = price;
            mLoss           = loss;

            for (int i = 0; i < spare; ++i)
                mOrders.add(0);
        }


        public float
        process(Vector<Fault> faults)
        {
            float   income  = 0;

            int     day     = 0;
            for (Fault f : faults)
            {
                for (; day < f.day(); income += processDay(day++))
                    ;   // NO BODY

                for (int c = f.count(); c > 0; --c)
                {
                    mWaiting.add(f.day());
                    mOrders.add(msArriveDays);
                }
            }

            for (; !mWaiting.isEmpty(); income += processDay(day++))
                ;   // NO BODY

            return income;
        }


        private float
        processDay(int day)
        {
            float   income  = 0;

            while (false == mOrders.isEmpty() && 0 == mOrders.peek()
                    && false == mWaiting.isEmpty())
            {
                mOrders.removeFirst();

                int waiting = Fix.FIFO == msFix
                        ? mWaiting.removeFirst()
                        : mWaiting.removeLast();
                income  = (day - waiting < warrantyDays())
                        ? mLoss : -mLoss;
            }

            for (ListIterator<Integer> oit = mOrders.listIterator();
                    oit.hasNext();
                    oit.set(max(0, oit.next() - 1))); // NO BODY

            return income;
        }


        public float
        expenses()
        {
            return mPrice * mSpare * msPeriodDays / msLifeDays;
        }


        private int     mSpare;
        private float   mPrice;
        private float   mLoss;

        private LinkedList<Integer> mOrders     =
                new LinkedList<Integer>();

        private LinkedList<Integer> mWaiting    =
                new LinkedList<Integer>();
    }


    public
    Part(String title, int faults, float price, float loss)
    {
        mTitle  = title;
        mFaults = faults;
        mPrice  = price;
        mLoss   = loss;


        // DEBUG
        Period p = simulateOnce();
        int sum = 0;
        for (Fault f : p.faults())
        {
            System.out.print(f + " ");
            sum += f.count();
        }
        System.out.println("~ " + sum + "/" + p.faults().size());
        for (Income i : p.income())
        {
            System.out.print(i.all() + ":" + i.pure() + " ");
        }
        System.out.println();
        System.out.println();
    }


    public Period
    simulateOnce()
    {
        Vector<Fault>   faults  = new Vector<Fault>(mFaults);

        int[]   distr   = getFaultDistribution();
        for (int day = 0; day < msPeriodDays; ++day)
        {
            int rnd = (int)(random() * 100);
            if (0 != distr[rnd])
                faults.add(new Fault(day, distr[rnd]));
        }

        return new Period(faults);
    }


    public int
    simulate(int count)
    {
        if (count < 1)
            count = 1;

        int sum = 0;
        for (int i = 0; i < count; ++i)
            sum += simulateOnce().income().size() - 1;

        return round(sum / count);
    }


    public String
    title()
    {
        return mTitle;
    }


    public int
    faults()
    {
        return mFaults;
    }


    public float
    faultProb()
    {
        return (float)mFaults / msPeriodDays * msFaultProbFactor;
    }


    public float
    price()
    {
        return mPrice * msPriceFactor;
    }


    public float
    loss()
    {
        return mLoss;
    }


    public String
    toString()
    {
        return title() + " (" + round(faultProb() * 100.0) + "%)";
    }


    public int
    compareTo(Part other)
    {
        return title().compareTo(other.title());
    }


    private int[]
    getFaultDistribution()
    {
        int[]   distr   = new int[100];

        float   lambda  = faultProb();
        for (int i = 0, n = 0, p = (int)round(puasson(lambda, n) * 100);
                i < 100 && p >= 0;
                i += p, p = (int)round(puasson(lambda, ++n) * 100))
        {
            for (int j = min(i + p, 100); j > i; distr[--j] = n)
                ;   // NO BODY
        }

        return distr;
    }



    ////// statics //////


    static public void
    faultProbFactor(float factor)
    {
        msFaultProbFactor   = factor;
    }


    static public void
    priceFactor(float factor)
    {
        msPriceFactor       = factor;
    }


    static public void
    periodDays(int days)
    {
        msPeriodDays        = days;
    }


    /*static public void
    delayDays(int days)
    {
        msDelayDays         = days;
    }*/


    static public void
    arriveDays(int days)
    {
        msArriveDays        = days;
    }


    static public void
    lifeDays(int days)
    {
        msLifeDays          = days;
    }


    static public void
    warranty(boolean enabled)
    {
        msWarranty          = enabled;
    }


    static public void
    fix(Fix fix)
    {
        msFix   = fix;
    }


    static public float
    faultProbFactor()
    {
        return msFaultProbFactor;
    }


    static public float
    priceFactor()
    {
        return msPriceFactor;
    }


    static public int
    periodDays()
    {
        return msPeriodDays;
    }


    /*static public int
    delayDays()
    {
        return msDelayDays;
    }*/


    static public int
    warrantyDays()
    {
        if (msWarranty)
        {
            int rnd = (int)(random() * 105);
            if (rnd < 25)
                return 1;
            else if (rnd < 80)
                return 2;
            else if (rnd < 95)
                return 3;
            else
                return 5;
        }
        else
        {
            return 2;
        }
    }


    static public int
    arriveDays()
    {
        return msArriveDays;
    }


    static public int
    lifeDays()
    {
        return msLifeDays;
    }


    static public boolean
    warranty()
    {
        return msWarranty;
    }


    static public Fix
    fix()
    {
        return msFix;
    }


    static private double
    puasson(float lambda, int n)
    {
        return pow(lambda, n) / fact(n) * exp(-lambda);
    }


    static private double
    fact(long x)
    {
        long fact   = 1;
        for (long i = 2; i < x; fact *= i++);   // NO BODY

        return fact;
    }



    private String  mTitle;
    private int     mFaults;
    private float   mPrice;
    private float   mLoss;

    static private float    msFaultProbFactor   = 1;
    static private float    msPriceFactor       = 1;
    static private int      msPeriodDays        = 90;
    //static private int      msDelayDays         = 2;
    static private int      msArriveDays        = 5;
    static private int      msLifeDays          = 365 * 3; // 3 years
    static private boolean  msWarranty          = true;
    static private Fix      msFix               = Fix.FIFO;
}
